package ca.seia.comp6521.a1.externalmergesort;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ca.seia.comp6521.a1.io.FileReadResult;
import ca.seia.comp6521.a1.memory.FullBufferException;
import ca.seia.comp6521.a1.memory.InputMemoryBuffer;
import ca.seia.comp6521.a1.memory.OutputMemoryBuffer;
import ca.seia.comp6521.a1.util.MemoryCalculator;

public class PhaseIIPassController {

	int numberOfInputBuffers;
	int numberOfOutputBuffers;
	int sizeOfMainMemoryAllocated = 5;// 4Megabytes
	int integerCapacityOfInputBuffer;
	int integerCapacityOfOutputBuffer;
	int passNumber;

	List<InputMemoryBuffer> inputBuffers;
	List<OutputMemoryBuffer> outputBuffers;
	List<String> inputSublistFiles;
	List<String> outputSublistFiles;

	PhaseIStatistics phaseIResult;
	boolean isFirstPhaseIIPass;
	public String phaseIISortedDirectoryPath;

	public PhaseIIPassController(PhaseIStatistics phaseIResult,String phaseIISortedDirectoryPath) {
		this.phaseIResult = phaseIResult;
		this.inputBuffers = new ArrayList<>();
		this.outputBuffers = new ArrayList<>();
		this.outputSublistFiles = new ArrayList<>();
		this.inputSublistFiles = phaseIResult.getSortedSubListsFilePaths();
		this.isFirstPhaseIIPass = true;
		this.phaseIISortedDirectoryPath = phaseIISortedDirectoryPath;
		
		// TODO calculate the maximum capicity based on the memory and number of
		// sublists create optimizer class
		
		double numberOfSublistsPhaseI = phaseIResult.getSortedSubListsFilePaths().size();
		int maximumNumberOfIntegers = MemoryCalculator.getNumberOfPrimitiveInt(sizeOfMainMemoryAllocated);
		
		int integerCapacityOfBuffer =  (int) Math.floor(maximumNumberOfIntegers /(numberOfSublistsPhaseI  + 1 ));
		
		this.numberOfInputBuffers = (int) numberOfSublistsPhaseI;
		this.numberOfOutputBuffers = 1;
		//this.integerCapacityOfInputBuffer = 4681;
		//this.integerCapacityOfOutputBuffer = 4681;
		this.integerCapacityOfInputBuffer = integerCapacityOfBuffer;
		this.integerCapacityOfOutputBuffer = integerCapacityOfBuffer;
		System.out.println("IntegerCapacityOfBuffer = " + integerCapacityOfBuffer +" integers");
		System.out.println("NumberOfInputBuffers    = " + numberOfInputBuffers +" buffers");
		System.out.println("NumberOfOutputBuffers   = " + numberOfOutputBuffers +" buffers");
	}

	

	public PhaseIIPassResult executePhaseIIPass(PhaseIIPassResult previousPassResult) throws FileNotFoundException {
		long startTime = System.currentTimeMillis();
		PhaseIIPassResult passResult = new PhaseIIPassResult();
		List<String> subListFilesToProcess = null;
		this.createInputBuffers();
		this.createOutputBuffers();
		if (isFirstPhaseIIPass) {
			isFirstPhaseIIPass = false;
			subListFilesToProcess = phaseIResult.getSortedSubListsFilePaths();
			this.passNumber = 1;
		} else {
			subListFilesToProcess = previousPassResult.getSortedSubListFiles();
			this.passNumber = previousPassResult.getPassNumber() + 1;
		}

		int nbrOfSortedSubListsToProduce = getNbrOfSortedSubListsToProduce(subListFilesToProcess.size(), this.numberOfInputBuffers);
		
		for (int sortedSubListCount = 0; sortedSubListCount < nbrOfSortedSubListsToProduce; sortedSubListCount++) {
			String subListFileName = generateSortedSubListFile(subListFilesToProcess,sortedSubListCount);
			passResult.getSortedSubListFiles().add(subListFileName);
		}
		passResult.setPassNumber(this.passNumber);
		long endtime = System.currentTimeMillis() - startTime;
		passResult.setExecutionTime(endtime);
		return passResult;
	}

	private List<InputMemoryBuffer> assignSubListsToInputBuffers(List<String> subListsToProcess,int sublistCount) throws FileNotFoundException {
		List<InputMemoryBuffer> assignedInputBuffers = new ArrayList<>();
		int startIndex = 0;
		if(subListsToProcess.size() <  this.numberOfInputBuffers) {
			startIndex = 0;
		} else {
			 startIndex = sublistCount * this.numberOfInputBuffers;
		}
		
		for(InputMemoryBuffer inputMemoryBuffer: this.inputBuffers) {
			if(startIndex == subListsToProcess.size()) {
				break;
			}
			inputMemoryBuffer.setAssignedSublistFile(subListsToProcess.get(startIndex));
			assignedInputBuffers.add(inputMemoryBuffer);
			startIndex++;
		}
		return assignedInputBuffers;
	}
	
	public String generateSortedSubListFile(List<String> subListFilesToProcess,int sublistCount) throws FileNotFoundException {
		boolean areAllInputBuffersAtEndOfFile = false;
		boolean isInputBufferAtEndOfFile = false;
			
		List<InputMemoryBuffer> assignedInputBuffers = assignSubListsToInputBuffers(subListFilesToProcess,sublistCount);
		String sortedSubListFile = generateSortedSubFileName(sublistCount);
		OutputMemoryBuffer outputBuffer01 = this.outputBuffers.get(0);
		
		// loading Buffers
		areAllInputBuffersAtEndOfFile = loadBuffers(assignedInputBuffers);

		//System.out.println("assinged buffers " + assignedInputBuffers);
		Map<InputMemoryBuffer, Integer> intermediateBuffer = createIntermediateBuffer(assignedInputBuffers);
		try {
			outputBuffer01.setAssignedSublistFile(sortedSubListFile);
			while (!areAllInputBuffersAtEndOfFile) {
				
				InputMemoryBuffer inputBufferWithSmallestValue = getInputMemoryBufferWithSmallestValue(intermediateBuffer);
				
				// write to buffer
				if (outputBuffer01.isBufferFull()) {
					outputBuffer01.write();
				}
				outputBuffer01.Insert(inputBufferWithSmallestValue.poll());
				
				if (outputBuffer01.isBufferFull()) {
					outputBuffer01.write();
				}
				
				//if the inputBuffer we have just removed the value from is empty we have to reload it
				if (inputBufferWithSmallestValue.isBufferFull()) {
					FileReadResult readResult = inputBufferWithSmallestValue.load();
//	bef ref				isInputBufferAtEndOfFile = inputBufferWithSmallestValue.load();
					isInputBufferAtEndOfFile = readResult.isEndOfFile();
					
					if (isInputBufferAtEndOfFile && inputBufferWithSmallestValue.isBufferFull()) {
						intermediateBuffer.remove(inputBufferWithSmallestValue);
					}
					if (intermediateBuffer.isEmpty()) {
						areAllInputBuffersAtEndOfFile = true;
					}
				}
			}
			System.out.println("final pass of output buffer");
			outputBuffer01.write();
		} catch (IOException | FullBufferException e) {
			e.printStackTrace();
		}
//		
//		for (InputMemoryBuffer inputMemoryBuffer : this.inputBuffers) {
//			System.out.println(inputMemoryBuffer.getId() + " size " + inputMemoryBuffer.getSize() );
//		}
//		
		return sortedSubListFile; 
	}

	public int getNbrOfSortedSubListsToProduce(int previousPassSubListCount, int inputBufferCount) {
		return (int) Math.ceil((previousPassSubListCount / (double) inputBufferCount));
	}

	private String generateSortedSubFileName(int sublistCount) {
		return phaseIISortedDirectoryPath  + "phase2_pass" + this.passNumber+"_sublist"+sublistCount+1+".dat";
	}
	
	private boolean loadBuffers(List<InputMemoryBuffer> inputMemoryBuffers) {
		if (inputMemoryBuffers == null || inputMemoryBuffers.size() == 0)
			return false;

		boolean areAllInputBuffersAtEndOfFile = true;
		boolean isInputBufferAtEndOfFile = false;
		for (InputMemoryBuffer inputMemoryBuffer : inputMemoryBuffers) {
			
//before Refactor			isInputBufferAtEndOfFile = inputMemoryBuffer.load();
			FileReadResult readResult = inputMemoryBuffer.load();
			isInputBufferAtEndOfFile = readResult.isEndOfFile();
			if (!isInputBufferAtEndOfFile || !(inputMemoryBuffer.isBufferFull())) {
				areAllInputBuffersAtEndOfFile = false;
			}
		}
		return areAllInputBuffersAtEndOfFile;
	}

	private InputMemoryBuffer getInputMemoryBufferWithSmallestValue(Map<InputMemoryBuffer, Integer> intermediateBuffer) {
		InputMemoryBuffer smallestInputMemoryBuffer = null;
		Integer smallestValue = null;
		Integer candidateValue = null;

		for (InputMemoryBuffer inputMemoryBuffer : intermediateBuffer.keySet()) {
//			if ( smallestValue != null && smallestValue == ExternalMergeSortExecutionController.UNUSED_NUMBER)
//				continue;
				
			if (smallestValue == null) {
				smallestValue = inputMemoryBuffer.peek();
				smallestInputMemoryBuffer = inputMemoryBuffer;
				continue;
			}
			candidateValue = inputMemoryBuffer.peek();
			if (smallestValue > candidateValue) {
				smallestValue = candidateValue;
				smallestInputMemoryBuffer = inputMemoryBuffer;
			}
		}
		return smallestInputMemoryBuffer;
	}

	private Map<InputMemoryBuffer,Integer> createIntermediateBuffer(List<InputMemoryBuffer> assignedInputBuffers) {
		Map<InputMemoryBuffer, Integer> intermediateBuffer = new HashMap<>();
		
		for (InputMemoryBuffer inputBuffer : assignedInputBuffers) {
			if(inputBuffer.getAssignedSublistFile() != null) {
//				System.out.println("Assigning inputbuffers");
//				System.out.println("id " + inputBuffer.getId());
//				System.out.println("first value of buffer " + inputBuffer.peek());
//				System.out.println("this pointer " + inputBuffer.pointer);
				intermediateBuffer.put(inputBuffer, inputBuffer.peek());
			}
		}
		return intermediateBuffer;
	}
	
	
	
	public void createInputBuffers() {
		for (int i = 0; i < this.numberOfInputBuffers; i++) {
			this.inputBuffers.add(new InputMemoryBuffer(this.integerCapacityOfInputBuffer, i));
		}
	}

	public void createOutputBuffers() {
		for (int i = 0; i < this.numberOfOutputBuffers; i++) {
			this.outputBuffers.add(new OutputMemoryBuffer(this.integerCapacityOfOutputBuffer));
		}
	}
	
	public int getNumberOfInputBuffers() {
		return numberOfInputBuffers;
	}

	public void setNumberOfInputBuffers(int numberOfInputBuffers) {
		this.numberOfInputBuffers = numberOfInputBuffers;
	}

	public int getNumberOfOutputBuffers() {
		return numberOfOutputBuffers;
	}

	public void setNumberOfOutputBuffers(int numberOfOutputBuffers) {
		this.numberOfOutputBuffers = numberOfOutputBuffers;
	}

	public int getIntegerCapacityOfInputBuffer() {
		return integerCapacityOfInputBuffer;
	}

	public void setIntegerCapacityOfInputBuffer(int integerCapacityOfInputBuffer) {
		this.integerCapacityOfInputBuffer = integerCapacityOfInputBuffer;
	}

	public int getIntegerCapacityOfOutputBuffer() {
		return integerCapacityOfOutputBuffer;
	}

	public void setIntegerCapacityOfOutputBuffer(int integerCapacityOfOutputBuffer) {
		this.integerCapacityOfOutputBuffer = integerCapacityOfOutputBuffer;
	}
}
